<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
    <title>SimpleNoteEditor v1.5</title>
    <style>

        :root{
            --bg-color: #ffffff;
            --fg-color: #000000;

            --btn-bar-bg: #ddd;
            --btn-bg-active: #ddd;
            --btn-bg: #fff;

            --quote-bg: #f9f9f9;
            --quote-fg: #666;
            --quote-line: #ccc;
        }

        /* 当用户系统偏好为深色模式时，应用以下样式 */
        @media (prefers-color-scheme: dark) {
          :root{
            --bg-color: #070707;
            --fg-color: #eee;

            --btn-bar-bg: #000;
            --btn-bg-active: #666;
            --btn-bg: #444;

            --quote-bg: #2e2e2e;
            --quote-fg: #bbb;
            --quote-line: #4d4d4d;
            }
        }


        body, html {
            margin: 0;
            padding: 0;
            height: 100%;
            background-color: transparent;
            color: var(--fg-color);
            word-break: normal;
            hyphenate-character: '-';
            hyphens: auto;
        }

        #editor {
            height: calc(100vh - 42px);
            width: 100%;
            padding: 12px;
            box-sizing: border-box;
            overflow-y: auto;
            outline: none;
        }

        #toolbar {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 44px;
            background-color: var(--btn-bar-bg);
            border-top: 1px solid var(--btn-bg-active);
            display: flex;
            align-items: center;
            padding: 3px 3px 5px 3px;
            box-sizing: border-box;
            overflow-x: auto;
            overflow-y: hidden;
            white-space: nowrap;
            z-index: 1000;
            /* 防止移动端长按按钮时出现菜单 */
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            user-select: none;
        }

        .toolbar-inner{
            height: 42px;
            flex-grow: 1;

            display: flex;
            align-items: center;

            box-sizing: border-box;

            overflow-x: auto;
            overflow-y: hidden;
            white-space: nowrap;

            /* 防止移动端长按按钮时出现菜单 */
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            user-select: none;
        }

        #toolbar button {
            width: 36px;
            height: 36px;
            margin: 0 3px;
            border: none;
            background-color: var(--btn-bg);
            cursor: pointer;
            border-radius: 4px;
            color: var(--fg-color);
            font-size: 16px;
            flex-shrink: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            line-height: 1;
            /* 添加过渡效果，使按压更平滑 */
            transition: background-color 0.1s, transform 0.1s;
        }

        /* --- v1.5 移动端适配：为所有设备添加按压样式 --- */
        #toolbar button:active {
            background-color: var(--btn-bg-active);
            transform: scale(0.98);
        }

        .tickbefore::before { content: "√"; font-family: monospace; margin-left: -1em; display: inline-block; width: 1em; color: #00bb00; text-align: center;}
        .starbefore::before { content: "*"; font-family: monospace; margin-left: -1em; display: inline-block; width: 1em; text-align: center;}
        .dashbefore::before { content: "▪"; margin-left: -1em; display: inline-block; width: 1em; text-align: center;}
        .crossbefore::before { content: "×"; margin-left: -1em; display: inline-block; width: 1em; color: red;text-align: center; }
        .ongoingbefore::before { content: "→"; margin-left: -1em; display: inline-block; width: 1em; text-align: center; }
        .questionbefore::before { content: "?"; margin-left: -1em; display: inline-block; width: 1em; color: magenta; text-align: center;}
        .importantbefore::before { content: "!"; margin-left: -1em; display: inline-block; width: 1em; color: crimson; font-weight: bold;text-align: center; }
        .plusbefore::before { content: "+"; margin-left: -1em; display: inline-block; width: 1em;text-align: center; }

        .tickbefore, .starbefore, .dashbefore, .crossbefore, .ongoingbefore, .questionbefore, .importantbefore, .plusbefore {
            padding-left: 1em;
        }
        
        /* --- Other Styles --- */
        .blockquote {
            border-left: 4px solid var(--quote-line);
            padding: 0.5em 1em;
            margin: 1em 0;
            color: var(--quote-fg);
            background-color: var(--quote-bg);
        }
        
        .ph1 { font-size: 2em; font-weight: bold; }
        .ph2 { font-size: 1.5em; font-weight: bold; }
        .ph3 { font-size: 1.17em; font-weight: bold; }
        
        .lv1 { margin-left: 1.5em; }
        .lv2 { margin-left: 3em; }
        .lv3 { margin-left: 4.5em; }
        .lv4 { margin-left: 6em; }
        .lv5 { margin-left: 7.5em; }
        .lv6 { margin-left: 9em; }


    </style>
</head>

<body>
<!-- v1.5: 清除默认示例文本，保留一个空行 -->
<div id="editor" contenteditable="true">
    <div><br></div>
</div>
<div id="toolbar">
    <div class="toolbar-inner">

        <button data-action="h1">H1</button>
        <button data-action="h2">H2</button>
        <button data-action="h3">H3</button>
        <button data-action="blockquote">“</button>
        <button data-action="newline">↲</button>
        <button data-action="duplicate" style="font-size: 14px;">Dup</button>
        <button data-action="tick">√</button>
        <button data-action="star">*</button>
        <button data-action="dash">-</button>
        <button data-action="cross">×</button>
        <button data-action="ongoing">></button>
        <button data-action="question">?</button>
        <button data-action="important">!</button>
        <button data-action="plus">+</button>
        <button data-action="outdent">&lt;&lt;</button>
        <button data-action="indent">&gt;&gt;</button>
        <button data-action="move-up">↑</button>
        <button data-action="move-down">↓</button>

    </div>

    <button id="clear-style-btn" title="Clear Style">∅</button>
</div>
<script>
    document.addEventListener('DOMContentLoaded', () => {
        const editor = document.getElementById('editor');
        const toolbar = document.getElementById('toolbar');
        const clearStyleBtn = document.getElementById('clear-style-btn');

        // --- v1.5 新增：规范化辅助函数 ---

        /**
         * 规范化元素：移除div内部的span，只保留文本内容。
         * @param {HTMLElement} el - 需要规范化的div元素。
         */
        function normalizeElement(el) {
            if (el.nodeType === Node.ELEMENT_NODE && el.tagName === 'DIV' && el.querySelector('span')) {
                const textContent = el.innerText;
                el.innerHTML = '';
                el.textContent = textContent;
            }
            return el;
        }

        // 3. 核心辅助函数：获取目标元素 (已重写)
        function getTargetElements() {
            const selection = window.getSelection();
            if (!selection.rangeCount) return [];

            const range = selection.getRangeAt(0);
            if (range.collapsed) {
                const blockEl = getBlockElement(range.startContainer);
                if (!blockEl) return [];
                // 对获取到的元素进行规范化
                normalizeElement(blockEl);
                return [blockEl];
            }

            const startBlock = getBlockElement(range.startContainer);
            const endBlock = getBlockElement(range.endContainer);
            if (!startBlock || !endBlock) return [];

            const targets = [];
            let currentEl = startBlock;
            while (currentEl) {
                // 对获取到的每个元素进行规范化
                normalizeElement(currentEl);
                targets.push(currentEl);
                if (currentEl === endBlock) break;
                currentEl = currentEl.nextElementSibling;
            }
            return targets;
        }

        /**
         * 获取光标所在的块级元素。
         * v1.5 修订：如果遇到孤立的文本节点，会自动将其包裹进一个div，并返回新div。
         * @param {Node} node - 起始节点。
         * @returns {HTMLElement|null} - 代表一行的div元素，或null。
         */
        function getBlockElement(node) {
            if (node === editor) return null;
            let el = node;
            while (el && el.parentElement && el.parentElement !== editor) {
                el = el.parentElement;
            }
            if (el === editor) return null;

            // v1.5 新增：如果找到的是孤立的文本节点，则将其规范化为div
            if (el.nodeType === Node.TEXT_NODE) {
                const newDiv = document.createElement('div');
                newDiv.textContent = el.textContent;
                el.parentNode.replaceChild(newDiv, el);
                return newDiv;
            }
            return el;
        }

        // 4. 核心辅助函数：替换元素并保留内容和类
        function replaceElement(oldEl, newTagName) {
            const newEl = document.createElement(newTagName);
            newEl.className = oldEl.className;
            while (oldEl.firstChild) {
                newEl.appendChild(oldEl.firstChild);
            }
            oldEl.parentNode.replaceChild(newEl, oldEl);
            return newEl;
        }

        // 5. 互斥样式处理
        const MUTUALLY_EXCLUSIVE_CLASSES = ['blockquote', 'ph1', 'ph2', 'ph3', 'tickbefore', 'starbefore', 'dashbefore', 'crossbefore', 'ongoingbefore', 'questionbefore', 'importantbefore', 'plusbefore'];

        function applyExclusiveStyle(element, newClass) {
            if (element.tagName !== 'DIV') {
                element = replaceElement(element, 'DIV');
            }
            if (element.classList.contains(newClass)) {
                MUTUALLY_EXCLUSIVE_CLASSES.forEach(cls => element.classList.remove(cls));
            } else {
                MUTUALLY_EXCLUSIVE_CLASSES.forEach(cls => element.classList.remove(cls));
                element.classList.add(newClass);
            }
            if (element.className === '') {
                element.removeAttribute('class');
            }
        }

        // 6. 事件处理逻辑
        function handleAction(action) {
            const targets = getTargetElements();
            if (targets.length === 0) return;

            const selection = window.getSelection();
            const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;

            switch (action) {
                case 'h1':
                case 'h2':
                case 'h3': {
                    const className = `p${action}`;
                    targets.forEach(el => applyExclusiveStyle(el, className));
                    break;
                }
                case 'blockquote': {
                    targets.forEach(el => applyExclusiveStyle(el, 'blockquote'));
                    break;
                }
                case 'newline': {
                    const targetEl = targets[targets.length - 1];
                    if (!targetEl) return;

                    const newEl = document.createElement('div');
                    newEl.innerHTML = '<br>';
                    targetEl.parentNode.insertBefore(newEl, targetEl.nextSibling);

                    const newRange = document.createRange();
                    newRange.selectNodeContents(newEl);
                    newRange.collapse(true);
                    selection.removeAllRanges();
                    selection.addRange(newRange);
                    break;
                }
                case 'duplicate': {
                    if (targets.length !== 1 || !range || !range.collapsed) return;

                    const originalEl = targets[0];
                    
                    const marker = document.createElement('span');
                    marker.id = 'dup-cursor-marker';
                    marker.style.cssText = 'width:0;height:0;overflow:hidden;display:inline;';
                    range.insertNode(marker);

                    const newEl = document.createElement('div');
                    newEl.className = originalEl.className;
                    newEl.innerHTML = originalEl.innerHTML;

                    originalEl.parentNode.insertBefore(newEl, originalEl.nextSibling);

                    const newMarker = newEl.querySelector('#dup-cursor-marker');
                    if (newMarker) {
                        const newRange = document.createRange();
                        newRange.setStartBefore(newMarker);
                        newRange.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(newRange);
                    }

                    document.querySelectorAll('#dup-cursor-marker').forEach(m => m.remove());
                    break;
                }
                case 'tick':
                case 'star':
                case 'dash':
                case 'cross':
                case 'ongoing':
                case 'question':
                case 'important':
                case 'plus': {
                    const classNameMap = {
                        tick: 'tickbefore', star: 'starbefore', dash: 'dashbefore',
                        cross: 'crossbefore', ongoing: 'ongoingbefore', question: 'questionbefore',
                        important: 'importantbefore', plus: 'plusbefore'
                    };
                    const className = classNameMap[action];
                    targets.forEach(el => applyExclusiveStyle(el, className));
                    break;
                }
                case 'indent':
                case 'outdent': {
                    const direction = action === 'indent' ? 1 : -1;
                    targets.forEach(el => {
                        if (el.tagName !== 'DIV') {
                            el = replaceElement(el, 'DIV');
                        }
                        if (el.classList.contains('blockquote')) {
                            el.classList.remove('blockquote');
                            if (el.className === '') el.removeAttribute('class');
                        }

                        let currentLevel = 0;
                        for (let i = 1; i <= 6; i++) {
                            if (el.classList.contains(`lv${i}`)) {
                                currentLevel = i;
                                break;
                            }
                        }

                        let newLevel = currentLevel + direction;
                        if (newLevel > 0 && newLevel <= 6) {
                            if (currentLevel > 0) el.classList.remove(`lv${currentLevel}`);
                            el.classList.add(`lv${newLevel}`);
                        } else if (newLevel <= 0 && currentLevel > 0) {
                            el.classList.remove(`lv${currentLevel}`);
                        }

                        if (el.className === '') el.removeAttribute('class');
                    });
                    break;
                }
                case 'move-up':
                case 'move-down': {
                    if (!range || !range.collapsed) return;
                    const target = targets[0];
                    if (!target) return;

                    const sibling = action === 'move-up' ? target.previousElementSibling : target.nextElementSibling;
                    if (!sibling) return;

                    const startContainer = range.startContainer;
                    const startOffset = range.startOffset;

                    if (action === 'move-up') {
                        editor.insertBefore(target, sibling);
                    } else {
                        editor.insertBefore(sibling, target);
                    }

                    try {
                        range.setStart(startContainer, startOffset);
                        range.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(range);
                    } catch (e) {
                        range.selectNodeContents(target);
                        range.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(range);
                    }
                    break;
                }
            }
        }

        // 7. 绑定事件监听器
        toolbar.addEventListener('click', (e) => {
            if (e.target.tagName === 'BUTTON' && e.target.dataset.action) {
                e.preventDefault();
                handleAction(e.target.dataset.action);
            }
        });
        toolbar.addEventListener('mousedown', (e) => e.preventDefault());
        
        clearStyleBtn.addEventListener('click', (e) => {
            e.preventDefault();
            const targets = getTargetElements();
            if (targets.length === 0) return;

            targets.forEach(el => {
                if (el.tagName !== 'DIV') {
                    replaceElement(el, 'DIV');
                } else {
                    el.className = '';
                    el.removeAttribute('class');
                }
            });
        });
        clearStyleBtn.addEventListener('mousedown', (e) => e.preventDefault());


        // 8. 添加全局API方法
        /**
         * v1.5 修订：在保存时会处理孤立的文本节点。
         * @returns {Array} - 编辑器内容的数组表示。
         */
        window.getEditorContent = function() {
            const content = [];
            // 改为遍历 childNodes 以捕获文本节点
            for (const childNode of editor.childNodes) {
                if (childNode.nodeType === Node.ELEMENT_NODE) {
                    content.push({
                        classes: childNode.className.split(' ').filter(Boolean),
                        content: childNode.innerHTML
                    });
                } else if (childNode.nodeType === Node.TEXT_NODE) {
                    // 新增逻辑：处理孤立的非空文本节点
                    if (childNode.textContent.trim() !== '') {
                        content.push({
                            classes: [],
                            content: childNode.textContent
                        });
                    }
                }
            }
            return content;
        };

        window.setEditorContent = function(data) {
            if (!Array.isArray(data)) {
                console.error("Invalid data format for setEditorContent. Expected an array.");
                return;
            }
            editor.innerHTML = '';
            data.forEach(item => {
                const newEl = document.createElement('div');
                if (item.classes && Array.isArray(item.classes)) {
                    newEl.className = item.classes.join(' ');
                }
                if (item.content) {
                    newEl.innerHTML = item.content;
                }
                editor.appendChild(newEl);
            });
        };
        
		window.getCurrentLineNumber = function() {
			const selection = window.getSelection();
			if (!selection.rangeCount) return 0;
			const range = selection.getRangeAt(0);
			const blockEl = getBlockElement(range.startContainer);
			if (!blockEl) return 0;
			const children = Array.from(editor.children);
			const lineNumber = children.indexOf(blockEl) + 1;
			return lineNumber > 0 ? lineNumber : 0;
		};
		
		window.setCurrentLineNumber = function(line) {
			if (typeof line !== 'number' || line < 1) return;
			const children = editor.children;
			if (line > children.length) return;
			const targetElement = children[line - 1];
			if (!targetElement) return;
			const newRange = document.createRange();
			newRange.selectNodeContents(targetElement);
			newRange.collapse(true);
			const selection = window.getSelection();
			selection.removeAllRanges();
			selection.addRange(newRange);
			targetElement.scrollIntoView({ behavior: 'instant', block: 'center' });
		};
		
        // --- v1.5 新增：测试方法 ---
        /**
         * 测试保存和加载功能，用于验证数据一致性。
         */
        window.testSaveAndLoad = function() {
            console.log("Testing save and load...");
            const data = window.getEditorContent();
            console.log("Saved data:", data);
            window.setEditorContent(data);
            console.log("Load complete.");
        };

		// --- v1.4 Bug Fix: 修复粘贴时空行消失的问题 ---
		function traverseAndFlatten(node, result) {
			var lastIsText = false;
			for (const childNode of node.childNodes) {
				if (childNode.nodeType === Node.ELEMENT_NODE && childNode.tagName === 'BR') {
					lastIsText = false;
					continue;
				}
				if (childNode.nodeType === Node.ELEMENT_NODE) {
					lastIsText = false;
					const isSingleBrDiv = childNode.tagName === 'DIV' &&
										  childNode.childNodes.length === 1 &&
										  childNode.firstChild.tagName === 'BR';
					const isRegularContainer = childNode.querySelector(':scope > div, :scope > p');
					const isContainer = isRegularContainer && !isSingleBrDiv;
					if (isContainer) {
						traverseAndFlatten(childNode, result);
					} else {
						const classes = childNode.className ? childNode.className.split(' ').filter(Boolean) : [];
						let content = childNode.innerText;
						if (isSingleBrDiv) {
							content = '<br>';
						}
						result.push({ classes, content });
					}
				} else if (childNode.nodeType === Node.TEXT_NODE) {
					if(lastIsText){
						result[result.length - 1].content += childNode.textContent;
					}
					else if (childNode.textContent.trim() !== '') {
						result.push({ classes: [], content: childNode.textContent });
					}
					lastIsText = true;
				}
			}
		}
		editor.addEventListener('paste', (e) => {
			const clipboardData = e.clipboardData || window.clipboardData;
			const clipboardText = clipboardData ? clipboardData.getData('text/plain') : '';
			if (clipboardText && !clipboardText.includes('\n') && !clipboardText.includes('\r')) {
				return;
			}
			const totalLines = editor.children.length;
			const currentLine = window.getCurrentLineNumber();
			const relativePositionFromEnd = totalLines - currentLine;
			setTimeout(() => {
				const normalizedContent = [];
				traverseAndFlatten(editor, normalizedContent);
				window.setEditorContent(normalizedContent);
				setTimeout(() => {
					const newTotalLines = editor.children.length;
					const targetLine = newTotalLines - relativePositionFromEnd;
					if (targetLine >= 1) {
						window.setCurrentLineNumber(targetLine);
					} else {
						window.setCurrentLineNumber(1);
					}
				}, 0);
			}, 0);
		});
    });
</script>
<script src="android-extra.js"></script>
</body>
</html>
